1 module hip.api.net.hipnet; 2 public import hip.api.net.server; 3 4 5 /** 6 * Network module is one which can't be abstracted much. 7 * That happens because, by using templates, it is possible to reduce the 8 * memory footprint required for sending and getting data. 9 * The implementation of sending data without templates would likely require to allocate memory 10 * on heap instead of stack. 11 * 12 * So, most functionality will actually be implemented on API instead of inside the engine module. 13 * 14 */ 15 16 17 enum NetConnectStatus 18 { 19 ///Represents basically a blank state where it can transition to connected or waiting 20 disconnected, 21 ///It is currently connected and getting data will actually send the data instead of trying to connect. 22 connected, 23 ///It is attempting to connect to the specified IP+ID Address 24 waiting, 25 ///It was connected, but for some reason, client sent a disconnect message and now it is trying to reconnect 26 attemptingReconnect 27 } 28 29 30 enum IPType : ubyte 31 { 32 ipv4, 33 ipv6 34 } 35 36 struct NetIPAddress 37 { 38 string ip; 39 ushort port; 40 IPType type = IPType.ipv4; 41 } 42 43 /** 44 * Currently, tcp is used everywhere and websockets are used on WASM 45 */ 46 enum NetInterface 47 { 48 automatic, 49 tcp, 50 websocket 51 } 52 53 /** 54 * Those are the NetIDs that your server must implement. 55 * Basically, 0 is ought to be a message that the server must interpret 56 * While 1 is the broadcast one. The IDs are mostly relevant to WebSockets 57 */ 58 enum NetID : uint 59 { 60 server, 61 broadcast, 62 start, 63 end = uint.max 64 } 65 66 /** 67 * Currently, only binary seems to be relevant. 68 * Custom + might be reserved for implementing known formats (such as JSON) 69 */ 70 package enum NetDataType : ubyte 71 { 72 binary, 73 text, 74 custom 75 } 76 77 78 /** 79 * Usage of Alignment(1) can reduce the memory footprint. 80 * It also should use a fixed size type since there will be no surprises when running 81 * the code via another platform or something like that. 82 */ 83 struct NetHeader 84 { 85 align(1): 86 uint length; 87 NetDataType type; 88 89 bool invalid() const { return length == 0; } 90 } 91 92 struct NetConnectInfo 93 { 94 NetIPAddress ip; 95 uint id; 96 } 97 98 template NetBinding(Req, Resp) 99 { 100 alias Request = Req; 101 alias Response = Resp; 102 } 103 104 105 struct NetBuffer 106 { 107 ///The header may change after some time since it will be reused. 108 NetHeader header; 109 ///Buffer may be bigger than expected size as it will be reused 110 ubyte[] buffer; 111 112 size_t expectedSize() const {return header.length; } 113 bool isInvalid() const { return header.invalid; } 114 115 ///Gets the buffer only if it has already finished, and sliced to its expected size 116 ubyte[] getFinishedBuffer() 117 { 118 if(expectedSize == 0) 119 return null; 120 return buffer[0..expectedSize]; 121 } 122 } 123 124 /** 125 * Those are the reserved type IDs that are found in every MarkNetData instance. 126 * Since a new instance of this enum is created, the reserved types are written 127 * snake-cased. 128 * Custom type members will have the exact same name as the type they intend to use. 129 * 130 * connect: void send_connect(INetwork) 131 * disconnect: void send_disconnect(INetwork) 132 * get_connected_clients: send_get_connected_clients(INetwork) 133 * client_connect: send_client_connect(INetwork, uint targetID) 134 */ 135 enum MarkedNetReservedTypes : ubyte 136 { 137 invalid, 138 ///Send that message so NetController can identify that a network connection was established. 139 @NetBinding!(void, void) connect, 140 ///Send that message so NetController can identify that a network interface was disconnected 141 @NetBinding!(void, void) disconnect, 142 ///Sends a message to the server requesting for the available connection IDs 143 @NetBinding!(void, ConnectedClientsResponse) get_connected_clients, 144 ///The ID to connect to. Must be a valid ID received from get_connected_clients 145 @NetBinding!(uint, ConnectToClientResponse) client_connect 146 } 147 148 template Attributes(T, string mem) 149 { 150 alias Attributes = __traits(getAttributes, __traits(getMember, T, mem)); 151 } 152 153 static foreach(m; __traits(allMembers, MarkedNetReservedTypes)) 154 { 155 static if(Attributes!(MarkedNetReservedTypes, m).length) 156 { 157 static if(is(Attributes!(MarkedNetReservedTypes, m)[0].Request == void)) 158 { 159 mixin("void send_",m,"(INetwork net){", 160 "net.sendDataToServer(__traits(getMember, MarkedNetReservedTypes, m));}"); 161 } 162 else 163 { 164 mixin("void send_",m,"(INetwork net, Attributes!(MarkedNetReservedTypes, m)[0].Request d){", 165 "pragma(LDC_no_typeinfo) static struct Data { align(1): MarkedNetReservedTypes t; Attributes!(MarkedNetReservedTypes, m)[0].Request data;} ", 166 "net.sendDataToServer(Data(__traits(getMember, MarkedNetReservedTypes, m), d));}"); 167 } 168 } 169 } 170 171 172 /** 173 * This function is only used inside MarkNetData, this allows to create automatically an enum which is used 174 * for being able to iterate type safely through its members on NetController. 175 * 176 * Params: 177 * enumName = Enum name that will be generated 178 * enumType = The enum type 179 * Returns: A string to be mixed which defines a new enum. 180 */ 181 private string enumFromTypes(T...)(string enumName, string enumType) 182 { 183 string ret = "enum "~enumName~": "~enumType~"{"; 184 foreach(mem; __traits(allMembers, MarkedNetReservedTypes)) 185 ret~= mem ~ ", "; 186 static foreach(t; T) 187 { 188 static assert(is(t == struct) || is(t == enum), "MarkNetData only works for enums and structs."); 189 ret~= t.stringof~","; 190 } 191 return ret~"}"; 192 } 193 194 195 /** 196 * Creates a set of types which 197 * Params: 198 * T = The types that are valid to send on network. 199 */ 200 template MarkNetData(T...) 201 { 202 enum PredefinedTypesCount = __traits(allMembers, MarkedNetReservedTypes).length; 203 204 static if(T.length <= ubyte.max) 205 alias idType = ubyte; 206 else static if(T.length <= ushort.max) 207 alias idType = ushort; 208 209 mixin(enumFromTypes!(T)("Types", "idType")); 210 211 string getTypeName(idType v) 212 { 213 static foreach(t; T) 214 { 215 if(v == __traits(getMember, Types, t.stringof)) 216 return t.stringof; 217 } 218 return "Unknown"; 219 } 220 221 bool isInvalid(idType v) 222 { 223 return v == 0 || v >= T.length; 224 } 225 226 static foreach(i, t; T) 227 { 228 void sendData(INetwork net, t data) 229 { 230 align(1) 231 static struct TypedData 232 { 233 t actualData; 234 idType typeID = i + PredefinedTypesCount; 235 } 236 net.sendData(TypedData(data, i+PredefinedTypesCount)); 237 } 238 } 239 240 /** 241 * To its data removing the type id. 242 * Params: 243 * buffer = The buffer that is used to take the type id and slices it to the actual data 244 * Returns: The type id from that buffer. Also enforces it is not invalid 245 */ 246 Types getDataFromBuffer(ref ubyte[] buffer) 247 { 248 idType typeID = *cast(idType*)(buffer.ptr + buffer.length - idType.sizeof); 249 buffer = buffer[0..buffer.length - idType.sizeof]; 250 return cast(Types)typeID; 251 } 252 253 254 255 bool isUnknown(Q)() 256 { 257 static foreach(t; T) 258 static if(is(t == Q)) 259 return false; 260 return true; 261 } 262 263 alias RegisteredTypes = T; 264 } 265 266 267 /** 268 * Implementation currently follows at `hip.network` 269 */ 270 interface INetwork 271 { 272 /** 273 * 274 * Params: 275 * ip = The IP to connect 276 * onConnect = Delegate to execute when connecting 277 * id = ID of the address to connect to. Relevant when you're not using a P2P connection. Enforced when using websockets, since direct connection is unavailable. 278 * Returns: The connection status 279 */ 280 NetConnectStatus connect(NetIPAddress ip, void delegate(INetwork) onConnect, uint id = NetID.server); 281 ///Gets ID for that network connection. It can't change over its lifetime 282 uint getConnectionSelfID() const; 283 ///Sets that network connection connected to the specified ID 284 void targetConnectionID(uint id); 285 uint targetConnectionID() const; 286 bool isHost() const; 287 ///Returns whether it has got any data 288 NetConnectStatus status() const; 289 NetInterface getNetInterfaceType() const; 290 NetConnectInfo getConnectInfo() const; 291 ///Returns if there is any data available to take 292 bool getData(); 293 ///Sends the data without modifying it further. It is called like that since it will be able to overload with templates 294 void sendDataRaw(ubyte[] data); 295 296 /** 297 * WARNING: It is important to call freeBuffer at some time since getting the buffer won't remove it from the queue 298 * This function must only be called after getData() returns true. 299 * Returns: A buffer stream or [a datagram buffer](This one is only on future) 300 */ 301 NetBuffer* getCompletedBuffer() const; 302 /** 303 * Make that buffer available inside the buffer pool and saves memory 304 * Params: 305 * buffer = A buffer inside the completed list 306 */ 307 void freeBuffer(NetBuffer* buffer); 308 309 /** 310 * Due to performance reasons, toNetworkBytes and toBytes needs to be in api instead of back implementation. 311 * This allows to use non-allocating memory when sending data. 312 * Keep in mind that the data is converted to big endian before being sent. 313 * Params: 314 * data = Any data type wanted to send 315 */ 316 void sendData(T)(T data) 317 { 318 import std.traits:isDynamicArray; 319 import hip.api.net.utils; 320 uint size = getSendTypeSize(data); 321 322 static if(is(T == string)) 323 { 324 NetHeader header = NetHeader(size, NetDataType.text); 325 } 326 else 327 { 328 static if(isDynamicArray!T) 329 { 330 NetHeader header = NetHeader(size, NetDataType.binary); 331 } 332 else 333 { 334 NetHeader header = NetHeader(size, NetDataType.binary); 335 } 336 } 337 sendDataRaw(toNetworkBytes(toBytes(header, data))); 338 } 339 340 final void withServerTarget(scope void delegate() dg) 341 { 342 uint currID = targetConnectionID; 343 targetConnectionID = NetID.server; 344 scope(exit) 345 targetConnectionID = currID; 346 dg(); 347 } 348 349 350 void sendDataToServer(T)(T data) 351 { 352 withServerTarget((){sendData(data);}); 353 } 354 355 void disconnect(); 356 final bool isConnected() const { return status == NetConnectStatus.connected; } 357 358 }